/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2012-2015 ForgeRock AS.
*/
package org.forgerock.openidm.workflow.activiti.impl;
import static org.forgerock.openidm.util.ResourceUtil.notSupported;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.delegate.JavaDelegate;
import org.activiti.engine.impl.ProcessEngineImpl;
import org.activiti.engine.impl.cfg.JtaProcessEngineConfiguration;
import org.activiti.engine.impl.interceptor.SessionFactory;
import org.activiti.engine.impl.scripting.ResolverFactory;
import org.activiti.engine.impl.scripting.ScriptBindingsFactory;
import org.activiti.osgi.OsgiScriptingEngines;
import org.activiti.osgi.blueprint.ProcessEngineFactory;
import org.apache.felix.scr.annotations.*;
import org.forgerock.openidm.datasource.DataSourceService;
import org.forgerock.openidm.router.IDMConnectionFactory;
import org.forgerock.services.context.Context;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.config.enhanced.EnhancedConfig;
import org.forgerock.openidm.config.enhanced.InvalidException;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.crypto.CryptoService;
import org.forgerock.openidm.router.RouteService;
import org.forgerock.script.ScriptRegistry;
import org.forgerock.openidm.workflow.activiti.impl.session.OpenIDMSessionFactory;
import org.forgerock.util.promise.Promise;
import org.h2.jdbcx.JdbcDataSource;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Workflow service implementation
*
* @version $Revision$ $Date$
*/
@Component(name = ActivitiServiceImpl.PID, immediate = true, policy = ConfigurationPolicy.REQUIRE)
@Service
@Properties({
@Property(name = Constants.SERVICE_DESCRIPTION, value = "Workflow Service"),
@Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME),
@Property(name = ServerConstants.ROUTER_PREFIX, value = {
ActivitiServiceImpl.ROUTER_PREFIX})})
@References({
@Reference(name = "JavaDelegateServiceReference", referenceInterface = JavaDelegate.class,
bind = "bindService", unbind = "unbindService",
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC),
@Reference(name = "ScriptRegistryService", referenceInterface = ScriptRegistry.class,
bind = "bindScriptRegistry", unbind = "unbindScriptRegistry",
cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC,
target = "(service.pid=org.forgerock.openidm.script)")
})
public class ActivitiServiceImpl implements RequestHandler {
final static Logger logger = LoggerFactory.getLogger(ActivitiServiceImpl.class);
public final static String PID = "org.forgerock.openidm.workflow";
public final static String ROUTER_PREFIX = "/workflow*";
// Keys in the JSON configuration
public static final String CONFIG_ENABLED = "enabled";
public static final String CONFIG_LOCATION = "location";
public static final String CONFIG_ENGINE = "engine";
public static final String CONFIG_ENGINE_URL = "engine/url";
public static final String CONFIG_ENGINE_USERNAME = "engine/username";
public static final String CONFIG_ENGINE_PASSWORD = "engine/password";
public static final String CONFIG_MAIL = "mail";
public static final String CONFIG_MAIL_HOST = "host";
public static final String CONFIG_MAIL_PORT = "port";
public static final String CONFIG_MAIL_USERNAME = "username";
public static final String CONFIG_MAIL_PASSWORD = "password";
public static final String CONFIG_MAIL_STARTTLS = "starttls";
public static final String CONFIG_TABLE_PREFIX = "tablePrefix";
public static final String CONFIG_TABLE_PREFIX_IS_SCHEMA = "tablePrefixIsSchema";
public static final String CONFIG_HISTORY = "history";
public static final String CONFIG_USE_DATASOURCE = "useDataSource";
public static final String CONFIG_WORKFLOWDIR = "workflowDirectory";
public static final String LOCALHOST = "localhost";
public static final int DEFAULT_MAIL_PORT = 25;
private boolean selfMadeProcessEngine = true;
@Reference(name = "processEngine", referenceInterface = ProcessEngine.class,
bind = "bindProcessEngine", unbind = "unbindProcessEngine",
cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.STATIC,
target = "(!openidm.activiti.engine=true)") //avoid registering the self made service
private ProcessEngine processEngine;
/**
* RepositoryService is a dependency of ConfigurationAdmin. Referencing the service here ensures the
* availability of this service during activation and deactivation to support the persistence of
* barInstallerConfiguration.
*/
@Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
private RepositoryService repositoryService = null;
@Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY,
bind = "bindConfigAdmin", unbind = "unbindConfigAdmin")
private ConfigurationAdmin configurationAdmin = null;
/**
* Some need to register a TransactionManager or we need to create one.
*/
@Reference(bind = "bindTransactionManager", unbind = "unbindTransactionManager")
private TransactionManager transactionManager;
@Reference(referenceInterface = DataSourceService.class,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
bind = "bindDataSourceService",
unbind = "unbindDataSourceService",
policy = ReferencePolicy.DYNAMIC,
strategy = ReferenceStrategy.EVENT)
private Map<String, DataSourceService> dataSourceServices = new HashMap<>();
protected void bindDataSourceService(DataSourceService service, Map properties) {
dataSourceServices.put(properties.get(ServerConstants.CONFIG_FACTORY_PID).toString(), service);
}
protected void unbindDataSourceService(DataSourceService service, Map properties) {
for (Map.Entry<String, DataSourceService> entry : dataSourceServices.entrySet()) {
if (service.equals(entry.getValue())) {
dataSourceServices.remove(entry.getKey());
break;
}
}
}
@Reference(target = "(" + ServerConstants.ROUTER_PREFIX + "=/managed)")
private RouteService routeService;
@Reference(policy = ReferencePolicy.DYNAMIC,
bind = "bindCryptoService", unbind = "unbindCryptoService")
CryptoService cryptoService;
@Reference(policy = ReferencePolicy.STATIC)
IDMConnectionFactory connectionFactory;
/** Enhanced configuration service. */
@Reference(policy = ReferencePolicy.DYNAMIC)
private EnhancedConfig enhancedConfig;
private final OpenIDMExpressionManager expressionManager = new OpenIDMExpressionManager();
private final SharedIdentityService identityService = new SharedIdentityService();
private final OpenIDMSessionFactory idmSessionFactory = new OpenIDMSessionFactory();
private ProcessEngineFactory processEngineFactory;
private Configuration barInstallerConfiguration;
private RequestHandler activitiResource;
//Configuration variables
private boolean enabled;
private EngineLocation location = EngineLocation.embedded;
private String url;
private String username;
private String password;
private String mailhost = LOCALHOST;
private int mailport = DEFAULT_MAIL_PORT;
private String mailusername;
private String mailpassword;
private boolean starttls;
private String tablePrefix;
private boolean tablePrefixIsSchema;
private String historyLevel;
private String useDataSource;
private String workflowDir;
@Override
public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
return activitiResource.handleAction(context, request);
}
@Override
public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
return activitiResource.handleCreate(context, request);
}
@Override
public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
return activitiResource.handleDelete(context, request);
}
@Override
public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
return notSupported(request).asPromise();
}
@Override
public Promise<QueryResponse, ResourceException> handleQuery(
Context context, QueryRequest request, QueryResourceHandler handler) {
return activitiResource.handleQuery(context, request, handler);
}
@Override
public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
return activitiResource.handleRead(context, request);
}
@Override
public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
return activitiResource.handleUpdate(context, request);
}
private enum EngineLocation {
embedded, local, remote
}
@Activate
void activate(ComponentContext compContext) {
logger.debug("Activating Service with configuration {}", compContext.getProperties());
try {
readConfiguration(compContext);
if (enabled) {
switch (location) {
case embedded: //start our embedded ProcessEngine
// see if we have the DataSourceService bound
final DataSourceService dataSourceService = dataSourceServices.get(useDataSource);
//we need a TransactionManager to use this
JtaProcessEngineConfiguration configuration = new JtaProcessEngineConfiguration();
if (null == dataSourceService) {
//initialise the default h2 DataSource
//Implement it here. There are examples in the JDBCRepoService
JdbcDataSource jdbcDataSource = new org.h2.jdbcx.JdbcDataSource();
File root = IdentityServer.getFileForWorkingPath("db/activiti/database");
jdbcDataSource.setURL("jdbc:h2:file:" + URLDecoder.decode(root.getPath(), "UTF-8")
+ ";DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=1000");
jdbcDataSource.setUser("sa");
configuration.setDatabaseType("h2");
configuration.setDataSource(jdbcDataSource);
} else {
// use DataSourceService as source of DataSource
configuration.setDataSource(dataSourceService.getDataSource());
}
configuration.setIdentityService(identityService);
configuration.setTransactionManager(transactionManager);
configuration.setTransactionsExternallyManaged(true);
configuration.setDatabaseSchemaUpdate("true");
configuration.setDatabaseTablePrefix(tablePrefix);
configuration.setTablePrefixIsSchema(tablePrefixIsSchema);
List<SessionFactory> customSessionFactories = configuration.getCustomSessionFactories();
if (customSessionFactories == null) {
customSessionFactories = new ArrayList<SessionFactory>();
}
customSessionFactories.add(idmSessionFactory);
configuration.setCustomSessionFactories(customSessionFactories);
configuration.setExpressionManager(expressionManager);
configuration.setMailServerHost(mailhost);
configuration.setMailServerPort(mailport);
configuration.setMailServerUseTLS(starttls);
if (mailusername != null) {
configuration.setMailServerUsername(mailusername);
}
if (mailpassword != null) {
configuration.setMailServerPassword(mailpassword);
}
if (historyLevel != null) {
configuration.setHistory(historyLevel);
}
//needed for async workflows
configuration.setJobExecutorActivate(true);
processEngineFactory = new ProcessEngineFactory();
processEngineFactory.setProcessEngineConfiguration(configuration);
processEngineFactory.setBundle(compContext.getBundleContext().getBundle());
processEngineFactory.init();
//ScriptResolverFactory
List<ResolverFactory> resolverFactories = configuration.getResolverFactories();
resolverFactories.add(new OpenIDMResolverFactory());
configuration.setResolverFactories(resolverFactories);
configuration.getVariableTypes().addType(new JsonValueType());
configuration.setScriptingEngines(new OsgiScriptingEngines(new ScriptBindingsFactory(resolverFactories)));
//We are done!!
processEngine = processEngineFactory.getObject();
//We need to register the service because the Activiti-OSGi need this to deploy new BAR or BPMN
Hashtable<String, String> prop = new Hashtable<String, String>();
prop.put(Constants.SERVICE_PID, "org.forgerock.openidm.workflow.activiti.engine");
prop.put("openidm.activiti.engine", "true");
compContext.getBundleContext().registerService(ProcessEngine.class.getName(), processEngine, prop);
if (null != configurationAdmin) {
try {
barInstallerConfiguration = configurationAdmin.createFactoryConfiguration("org.apache.felix.fileinstall", null);
Dictionary<String, String> props = barInstallerConfiguration.getProperties();
if (props == null) {
props = new Hashtable<String, String>();
}
props.put("felix.fileinstall.poll", "2000");
props.put("felix.fileinstall.noInitialDelay", "true");
//TODO java.net.URLDecoder.decode(IdentityServer.getFileForPath("workflow").getAbsolutePath(),"UTF-8")
props.put("felix.fileinstall.dir", IdentityServer.getFileForInstallPath(workflowDir).getAbsolutePath());
props.put("felix.fileinstall.filter", ".*\\.bar|.*\\.xml");
props.put("felix.fileinstall.bundles.new.start", "true");
props.put("config.factory-pid", "activiti");
barInstallerConfiguration.update(props);
} catch (IOException ex) {
java.util.logging.Logger.getLogger(ActivitiServiceImpl.class.getName()).log(Level.SEVERE, null, ex);
}
}
activitiResource = new ActivitiResource(processEngine);
logger.debug("Activiti ProcessEngine is enabled");
break;
case local: //ProcessEngine is connected by @Reference
activitiResource = new ActivitiResource(processEngine);
break;
// case remote: //fetch remote connection parameters
// activitiResource = new HttpRemoteJsonResource(url, username, password);
// break;
default:
throw new InvalidException(CONFIG_LOCATION + " invalid, can not start workflow service.");
}
}
} catch (RuntimeException ex) {
logger.warn("Configuration invalid, can not start Activiti ProcessEngine service.", ex);
throw ex;
} catch (Exception ex) {
logger.warn("Configuration invalid, can not start Activiti ProcessEngine service.", ex);
throw new RuntimeException(ex);
}
}
@Deactivate
void deactivate(ComponentContext compContext) {
logger.debug("Deactivating Service {}", compContext.getProperties());
if (null != barInstallerConfiguration) {
try {
barInstallerConfiguration.delete();
} catch (IOException e) {
logger.error("Can not delete org.apache.felix.fileinstall-activiti configuration", e);
}
barInstallerConfiguration = null;
}
if (processEngine != null && "h2".equals(((ProcessEngineImpl)processEngine).getProcessEngineConfiguration().getDatabaseType() )) {
DataSource h2DdataSource = ((ProcessEngineImpl)processEngine).getProcessEngineConfiguration().getDataSource();
java.sql.Connection conn = null;
try {
conn = h2DdataSource.getConnection();
Statement stat = conn.createStatement();
stat.execute("SHUTDOWN");
stat.close();
} catch (SQLException ex) {
logger.warn("H2 database failed to stop properly", ex);
}
if (conn != null) {
try {
conn.close();
} catch (SQLException ex) {
logger.warn("H2 database failed to stop properly", ex);
}
}
}
if (null != processEngineFactory) {
try {
processEngineFactory.destroy();
} catch (Exception e) {
//Do Something?
}
}
logger.info(" Activiti ProcessEngine stopped.");
}
/**
* Read and process Workflow configuration file
*
* @param compContext
*/
private void readConfiguration(ComponentContext compContext) {
JsonValue config = enhancedConfig.getConfigurationAsJson(compContext);
if (!config.isNull()) {
enabled = config.get(CONFIG_ENABLED).defaultTo(true).asBoolean();
location = config.get(CONFIG_LOCATION).defaultTo(EngineLocation.embedded.name()).asEnum(EngineLocation.class);
useDataSource = config.get(CONFIG_USE_DATASOURCE).asString();
JsonValue mailconfig = config.get(CONFIG_MAIL);
if (mailconfig.isNotNull()) {
mailhost = mailconfig.get(CONFIG_MAIL_HOST).defaultTo(LOCALHOST).asString();
mailport = mailconfig.get(CONFIG_MAIL_PORT).defaultTo(DEFAULT_MAIL_PORT).asInteger();
mailusername = mailconfig.get(CONFIG_MAIL_USERNAME).asString();
mailpassword = mailconfig.get(CONFIG_MAIL_PASSWORD).asString();
starttls = mailconfig.get(CONFIG_MAIL_STARTTLS).defaultTo(false).asBoolean();
}
JsonValue engineConfig = config.get(CONFIG_ENGINE);
if (!engineConfig.isNull()) {
url = config.get(new JsonPointer(CONFIG_ENGINE_URL)).asString();
username = config.get(new JsonPointer(CONFIG_ENGINE_USERNAME)).asString();
password = config.get(new JsonPointer(CONFIG_ENGINE_PASSWORD)).asString();
}
tablePrefix = config.get(CONFIG_TABLE_PREFIX).defaultTo("").asString();
tablePrefixIsSchema = config.get(CONFIG_TABLE_PREFIX_IS_SCHEMA).defaultTo(false).asBoolean();
historyLevel = config.get(CONFIG_HISTORY).asString();
workflowDir = config.get(CONFIG_WORKFLOWDIR).defaultTo("workflow").asString();
}
}
//This method called before activate if there is a ProcessEngine service in the Service Registry
protected void bindProcessEngine(ProcessEngine processEngine) {
logger.info("Some other process already created the ProcessEngine so we don't need to make our own");
if (null == processEngine) {
this.processEngine = processEngine;
selfMadeProcessEngine = false;
}
}
protected void unbindProcessEngine(ProcessEngine processEngine) {
if (!selfMadeProcessEngine) {
this.processEngine = null;
this.activitiResource = null;
}
logger.info("ProcessEngine stopped.");
}
protected void bindScriptRegistry(ScriptRegistry scriptRegistry) {
this.idmSessionFactory.setScriptRegistry(scriptRegistry);
}
protected void unbindScriptRegistry(ScriptRegistry scriptRegistry) {
this.idmSessionFactory.setScriptRegistry(null);
}
public void bindService(JavaDelegate delegate, Map props) {
expressionManager.bindService(delegate, props);
}
public void unbindService(JavaDelegate delegate, Map props) {
expressionManager.unbindService(delegate, props);
}
public void bindTransactionManager(TransactionManager manager) {
transactionManager = manager;
}
public void unbindTransactionManager(TransactionManager manager) {
transactionManager = null;
}
public void bindConfigAdmin(ConfigurationAdmin configAdmin) {
this.configurationAdmin = configAdmin;
}
public void unbindConfigAdmin(ConfigurationAdmin configAdmin) {
this.configurationAdmin = null;
}
public void bindCryptoService(final CryptoService service) {
cryptoService = service;
identityService.setCryptoService(service);
}
public void unbindCryptoService(final CryptoService service) {
cryptoService = null;
identityService.setCryptoService(null);
}
protected void bindConnectionFactory(IDMConnectionFactory factory) {
connectionFactory = factory;
this.identityService.setConnectionFactory(factory);
}
protected void unbindConnectionFactory(IDMConnectionFactory factory) {
connectionFactory = null;
this.identityService.setConnectionFactory(null);
}
}